Przewidywanie wyników Oscarów (Best Picture)¶

Załadowanie potrzebnych pakietów:

In [1]:
import pandas as pd
import numpy as np
import seaborn as sns
from sklearn.preprocessing import MinMaxScaler
from sklearn.model_selection import StratifiedShuffleSplit
from sklearn.metrics import confusion_matrix, accuracy_score, roc_auc_score, precision_score, recall_score, f1_score
from sklearn.preprocessing import label_binarize
from sklearn.ensemble import GradientBoostingClassifier
import warnings
warnings.filterwarnings("ignore", category=UserWarning)
warnings.filterwarnings("ignore", category=RuntimeWarning)
import dalex as dx
%matplotlib inline

Zaczynam od załadowania dwóch datasetów. Jako, że drugi z nich zaczyna się dopiero od roku 1960, trzeba skrócić pierwszy z nich

In [2]:
imdb = pd.read_csv(r"C:\Users\dell\Desktop\studia uw\2 sem\ons\imdb.csv", sep = ",")
imdb = imdb[imdb['Year'] >= 1960]
imdb 
Out[2]:
Film Year Position Edition id_imdb rate rate_count duration genres country budget gross
220 The Apartment 1960 Winner 33 53604 8.3 163969.0 125.0 [' Comedy', ' Drama', ' Romance'] ['USA'] 3000000.0 1.877845e+07
221 The Alamo 1960 Nominee 33 53580 6.9 14366.0 162.0 [' Adventure', ' Drama', ' History', ' War', '... ['USA'] 12000000.0 NaN
222 Elmer Gantry 1960 Nominee 33 53793 7.8 10360.0 146.0 [' Drama'] ['USA'] 3000000.0 NaN
223 Sons and Lovers 1960 Nominee 33 54326 7.1 1399.0 103.0 [' Drama'] ['UK'] 500000.0 NaN
224 The Sundowners 1960 Nominee 33 54353 7.1 3769.0 133.0 [' Adventure', ' Drama'] ['UK', 'USA'] NaN NaN
... ... ... ... ... ... ... ... ... ... ... ... ...
558 Joker 2019 Nominee 92 7286456 8.5 933010.0 122.0 [' Crime', ' Drama', ' Thriller'] ['USA', 'Canada'] 55000000.0 1.074251e+09
559 Little Women 2019 Nominee 92 3281548 7.8 141728.0 135.0 [' Drama', ' Romance'] ['USA'] 40000000.0 2.166012e+08
560 Marriage Story 2019 Nominee 92 7653254 7.9 244940.0 137.0 [' Comedy', ' Drama', ' Romance'] ['UK', 'USA'] 18600000.0 3.336860e+05
561 1917 2019 Nominee 92 8579674 8.3 420872.0 119.0 [' Drama', ' Thriller', ' War'] ['USA', 'UK', 'India', 'Spain', 'Canada', 'Chi... 95000000.0 3.849118e+08
562 Once Upon a Time in Hollywood 2019 Nominee 92 7131622 7.6 547006.0 161.0 [' Comedy', ' Drama'] ['USA', 'UK', 'China'] 90000000.0 3.743436e+08

343 rows × 12 columns

In [3]:
obp = pd.read_csv(r"C:\Users\dell\Desktop\studia uw\2 sem\ons\oscardata_bestpicture.csv", sep = ",")
obp
Out[3]:
Category Film Nominee Winner Year Rating_IMDB Release_date Rating_rtaudience Rating_rtcritic Oscarstat_totalnoms ... Nonom_Criticschoice Nom_Criticschoice Nowin_SAG_bestcast Win_SAG_bestcast Nonom_SAG_bestcast Nom_SAG_bestcast Nowin_PGA Win_PGA Nonom_PGA Nom_PGA
0 Picture The Apartment The Apartment 1 1961 8.3 1960-09-16 94 93 10 ... 0 0 0 0 0 0 0 0 0 0
1 Picture The Alamo The Alamo 0 1961 6.9 1960-10-24 64 50 6 ... 0 0 0 0 0 0 0 0 0 0
2 Picture Elmer Gantry Elmer Gantry 0 1961 7.9 1960-07 86 96 5 ... 0 0 0 0 0 0 0 0 0 0
3 Picture Sons and Lovers Sons and Lovers 0 1961 7.3 1960-07-22 54 75 7 ... 0 0 0 0 0 0 0 0 0 0
4 Picture The Sundowners The Sundowners 0 1961 7.2 1961-02-28 62 78 5 ... 0 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
328 Picture A Star Is Born A Star Is Born 0 2019 8.0 2018-09-01 90 81 7 ... 0 1 1 0 0 1 1 0 0 1
329 Picture The Favourite The Favourite 0 2019 7.9 2018-12-01 93 62 10 ... 0 1 1 0 1 0 1 0 0 1
330 Picture Vice Vice 0 2019 7.1 2018-12-12 66 55 8 ... 0 1 1 0 1 0 1 0 0 1
331 Picture Roma Roma 0 2019 8.0 2018-11-21 96 83 10 ... 0 1 1 0 1 0 1 0 0 1
332 Picture Black Panther Black Panther 0 2019 7.4 2018-02-15 97 79 6 ... 0 1 0 1 0 1 1 0 0 1

333 rows × 60 columns

Niektóre nazwy kolumn mogą być niejasne:

  • rate_count - oznacza liczbę oddanych głosów na portalu IMDB
  • Rating_rtaudience - 'rt' w nazwie oznacza odniesienie do portalu Rotten Tomatoes
  • Release_Q1 - dotyczny kwartału, w którym film wszedł do kin
  • Nom_BAFTA - 'BAFTA' w nazwie oznacza Nagrodę Brytyjskiej Akademii Filmowej
  • Nom_DGA - oznacza nominację do Nagrody Amerykańskiej Gildii Reżyserów Filmowych
  • MPAA_G - nazwy zaczynające się od 'MPAA' odnoszą się do ograniczeń wiekowych
  • Win_SAG_bestcast - 'SAG' odnosi się do Nagrody Gildii Aktorów Filmowych
  • Nowin_PGA - 'PGA' w nazwie oznacza Nagrodę Amerykańskiej Gildii Producentów
  • Z reguły, jeśli jakiś film nie wygrał danej nagrody to ma wpisane 0 w komórkach typu 'Win_nagroda'. Są jednak nagrody, które powstały póżniej niż w 1960 roku i dla nich stworzono kolumny typu 'Nowin_nagroda' - w tej komórce film będzie miał wpisane 1 tylko jeśli miał szansę ją wygrać. Dla filmów, które powstały przed tą nagrodą będzie wpisane 0 zarówno przy 'Win_nagroda' jak i 'Nowin_nagroda'.

Okazuje się, że datasety różnią się wielkością, należy więc znaleźć różnice i je wyeliminować:

In [4]:
lista_a = list(imdb['Film'])
lista_a1 = list(obp['Film'])
In [5]:
roznica = list(set(lista_a) - set(lista_a1))
roznica
Out[5]:
['Jojo Rabbit',
 'Extremely Loud & Incredibly Close',
 'Crouching Tiger, Hidden Dragon',
 '1917',
 'Ford v Ferrari',
 "Precious: Based on the Novel 'Push' by Sapphire",
 'MASH',
 'Les Misérables',
 'Marriage Story',
 'Secrets & Lies',
 'Once Upon a Time in Hollywood',
 'Parasite',
 'The Postman (Il Postino)',
 'Little Women',
 'Joker',
 'The Irishman']
In [6]:
roznica2 = list(set(lista_a1) - set(lista_a))
roznica2
Out[6]:
['Precious',
 'Crouching Tiger, Hidden Dragon (Wo hu cang long)',
 'Il Postino: The Postman',
 'M*A*S*H',
 'Secrets and Lies',
 'Extremely Loud and Incredibly Close']

W przypadku tytułów: 'Once Upon a Time in Hollywood', '1917', 'Joker','Marriage Story', 'Little Women', 'Jojo Rabbit', 'The Irishman', 'Ford v Ferrari', 'Parasite' - różnica wynika z tego że w jendym datasecie w "Year" jest wpisana data nagrody a w drugim data premiery filmu

In [7]:
do_usuniecia = ['Once Upon a Time in Hollywood', '1917', 'Joker',
 'Marriage Story', 'Little Women', 'Jojo Rabbit',
 'The Irishman', 'Ford v Ferrari', 'Parasite', 'Les Misérables']
In [8]:
for x in lista_a:
    if x in do_usuniecia:
        idx = imdb.index[imdb['Film']==x].tolist()[0]
        imdb = imdb.drop([idx], axis=0)#, inplace=True)
        #print(idx)
In [9]:
imdb.tail(10)
Out[9]:
Film Year Position Edition id_imdb rate rate_count duration genres country budget gross
544 The Post 2017 Nominee 90 6294822 7.2 135510.0 116.0 [' Drama'] ['USA', 'UK'] 50000000.0 1.797695e+08
545 Three Billboards Outside Ebbing, Missouri 2017 Nominee 90 5027774 8.2 430452.0 115.0 [' Comedy', ' Crime', ' Drama'] ['UK', 'USA'] 15000000.0 1.601920e+08
546 Green Book 2018 Winner 91 6966692 8.2 374447.0 130.0 [' Biography', ' Comedy', ' Drama', ' Music'] ['USA', 'China'] 23000000.0 3.217527e+08
547 Black Panther 2018 Nominee 91 1825683 7.3 623682.0 134.0 [' Action', ' Adventure', ' Sci-Fi'] ['USA'] 200000000.0 1.347598e+09
548 BlacKkKlansman 2018 Nominee 91 7349662 7.5 221961.0 135.0 [' Biography', ' Comedy', ' Crime', ' Drama'] ['USA'] 15000000.0 9.340082e+07
549 Bohemian Rhapsody 2018 Nominee 91 1727824 8.0 448781.0 134.0 [' Biography', ' Drama', ' Music'] ['UK', 'USA'] 52000000.0 9.052345e+08
550 The Favourite 2018 Nominee 91 5083738 7.5 171605.0 119.0 [' Biography', ' Comedy', ' Drama', ' History'] ['Ireland', 'UK', 'USA'] 15000000.0 9.591871e+07
551 Roma 2018 Nominee 91 6155172 7.7 139905.0 135.0 [' Drama'] ['Mexico', 'USA'] NaN 1.140769e+06
552 A Star Is Born 2018 Nominee 91 1517451 7.6 333071.0 136.0 [' Drama', ' Music', ' Romance'] ['USA'] 36000000.0 4.361889e+08
553 Vice 2018 Nominee 91 6266538 7.2 121358.0 132.0 [' Biography', ' Comedy', ' Drama'] ['USA'] 60000000.0 7.607349e+07

W pozostałych przypadkach różnica wynika z różnego zapisu tytułu, poprawiam więc je ręcznie:

In [10]:
idx = imdb.index[imdb['Film']=="Precious: Based on the Novel 'Push' by Sapphire"].tolist()[0]
imdb.at[idx,'Film'] = 'Precious'
idx = imdb.index[imdb['Film']=='The Postman (Il Postino)'].tolist()[0]
imdb.at[idx,'Film'] = 'Il Postino: The Postman'
idx = imdb.index[imdb['Film']=='Secrets & Lies'].tolist()[0]
imdb.at[idx,'Film'] = 'Secrets and Lies'
idx = imdb.index[imdb['Film']=='MASH'].tolist()[0]
imdb.at[idx,'Film'] = 'M*A*S*H'
idx = imdb.index[imdb['Film']=='Crouching Tiger, Hidden Dragon'].tolist()[0]
imdb.at[idx,'Film'] = 'Crouching Tiger, Hidden Dragon (Wo hu cang long)'
idx = imdb.index[imdb['Film']=='Extremely Loud & Incredibly Close'].tolist()[0]
imdb.at[idx,'Film'] = 'Extremely Loud and Incredibly Close'
In [11]:
print(len(obp), len(imdb)) # teraz już rozmiary się zgadzają
333 333

Usuwam zbędne koumny:

In [12]:
obp.drop(['Category', 'Nominee'], axis=1, inplace=True)
imdb.drop(['Year'], axis=1, inplace=True)
In [13]:
imdb = imdb.set_index('Film')
obp = obp.set_index('Film')

Następnie łączę DataFrame'y na podstawie nazwy filmu:

In [14]:
data = pd.merge(imdb, obp, left_index=True, right_index=True)
data
Out[14]:
Position Edition id_imdb rate rate_count duration genres country budget gross ... Nonom_Criticschoice Nom_Criticschoice Nowin_SAG_bestcast Win_SAG_bestcast Nonom_SAG_bestcast Nom_SAG_bestcast Nowin_PGA Win_PGA Nonom_PGA Nom_PGA
Film
The Apartment Winner 33 53604 8.3 163969.0 125.0 [' Comedy', ' Drama', ' Romance'] ['USA'] 3000000.0 18778454.0 ... 0 0 0 0 0 0 0 0 0 0
The Alamo Nominee 33 53580 6.9 14366.0 162.0 [' Adventure', ' Drama', ' History', ' War', '... ['USA'] 12000000.0 NaN ... 0 0 0 0 0 0 0 0 0 0
Elmer Gantry Nominee 33 53793 7.8 10360.0 146.0 [' Drama'] ['USA'] 3000000.0 NaN ... 0 0 0 0 0 0 0 0 0 0
Sons and Lovers Nominee 33 54326 7.1 1399.0 103.0 [' Drama'] ['UK'] 500000.0 NaN ... 0 0 0 0 0 0 0 0 0 0
The Sundowners Nominee 33 54353 7.1 3769.0 133.0 [' Adventure', ' Drama'] ['UK', 'USA'] NaN NaN ... 0 0 0 0 0 0 0 0 0 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
Bohemian Rhapsody Nominee 91 1727824 8.0 448781.0 134.0 [' Biography', ' Drama', ' Music'] ['UK', 'USA'] 52000000.0 905234477.0 ... 1 0 1 0 0 1 1 0 0 1
The Favourite Nominee 91 5083738 7.5 171605.0 119.0 [' Biography', ' Comedy', ' Drama', ' History'] ['Ireland', 'UK', 'USA'] 15000000.0 95918706.0 ... 0 1 1 0 1 0 1 0 0 1
Roma Nominee 91 6155172 7.7 139905.0 135.0 [' Drama'] ['Mexico', 'USA'] NaN 1140769.0 ... 0 1 1 0 1 0 1 0 0 1
A Star Is Born Nominee 91 1517451 7.6 333071.0 136.0 [' Drama', ' Music', ' Romance'] ['USA'] 36000000.0 436188866.0 ... 0 1 1 0 0 1 1 0 0 1
Vice Nominee 91 6266538 7.2 121358.0 132.0 [' Biography', ' Comedy', ' Drama'] ['USA'] 60000000.0 76073488.0 ... 0 1 1 0 1 0 1 0 0 1

333 rows × 67 columns

In [15]:
data.drop(['Year', 'Position', 'Edition', 'id_imdb', 'genres', 'rate',
           'Release_date', 'MPAA_rating'], axis=1, inplace=True)

Zapis dotyczący krajów produkcji należy zmienić. Uznałam, że najlepiej będzie zrobić podobnie jak jest w przypadku gatunków filmowych, tzn. dodajemy tyle kolumn ile jest krajów i w zależności od tego, czy dany kraj był pierwotnie wymieniony, dane pole jest wypełnione wartością 1 lub 0.

In [16]:
kraje = list(data['country'])
kraje[4]
Out[16]:
"['UK', 'USA']"

(Proszę wybaczyć zapis kodu, ale wykonanie tego ręcznie na tę chwilę okazało się dla mnie najszybsze)

In [17]:
USA = []
UK = []
Switzerland = []
Greece = []
Italy = []
France = []
Sweden = []
Canada = []
India = []
Mexico = []
Japan = []
Ireland = []
New_Zealand = []
Australia = []
Germany = []
Hong_Kong = []
China = []
for a in kraje:
    try:
        a.index('USA')
    except ValueError:
        USA.append(0)
    else:
        USA.append(1)
        
    try:
        a.index('UK')
    except ValueError:
        UK.append(0)
    else:
        UK.append(1)
    try:
        a.index('Switzerland')
    except ValueError:
        Switzerland.append(0)
    else:
        Switzerland.append(1)
    try:
        a.index('Greece')
    except ValueError:
        Greece.append(0)
    else:
        Greece.append(1)
    try:
        a.index('Italy')
    except ValueError:
        Italy.append(0)
    else:
        Italy.append(1)
    try:
        a.index('France')
    except ValueError:
        France.append(0)
    else:
        France.append(1)
    try:
        a.index('Sweden')
    except ValueError:
        Sweden.append(0)
    else:
        Sweden.append(1)
    try:
        a.index('Canada')
    except ValueError:
        Canada.append(0)
    else:
        Canada.append(1)
    try:
        a.index('Japan')
    except ValueError:
        Japan.append(0)
    else:
        Japan.append(1)
    try:
        a.index('Ireland')
    except ValueError:
        Ireland.append(0)
    else:
        Ireland.append(1)
    try:
        a.index('New Zealand')
    except ValueError:
        New_Zealand.append(0)
    else:
        New_Zealand.append(1)
    try:
        a.index('Australia')
    except ValueError:
        Australia.append(0)
    else:
        Australia.append(1)
    try:
        a.index('Germany')
    except ValueError:
        Germany.append(0)
    else:
        Germany.append(1)
    try:
        a.index('Hong Kong')
    except ValueError:
        Hong_Kong.append(0)
    else:
        Hong_Kong.append(1)
    try:
        a.index('China')
    except ValueError:
        China.append(0)
    else:
        China.append(1)
    try:
        a.index('India')
    except ValueError:
        India.append(0)
    else:
        India.append(1)
    try:
        a.index('Mexico')
    except ValueError:
        Mexico.append(0)
    else:
        Mexico.append(1)
In [18]:
len(New_Zealand)
Out[18]:
333
In [19]:
data['prod_USA'] = USA
data['prod_UK'] = UK
data['prod_Switzerland'] = Switzerland
data['prod_Greece'] = Greece
data['prod_Italy'] = Italy
data['prod_France'] = France
data['prod_Sweden'] = Sweden
data['prod_Canada'] = Canada
data['prod_India'] = India
data['prod_Mexico'] = Mexico
data['prod_Japan'] = Japan
data['prod_Ireland'] = Ireland
data['prod_New_Zealand'] = New_Zealand
data['prod_Australia'] = Australia
data['prod_Germany'] = Germany
data['prod_Hong_Kong'] = Hong_Kong
data['prod_China'] = China
data.drop(['country'], axis=1, inplace=True)
In [20]:
data.head(8)
Out[20]:
rate_count duration budget gross Winner Rating_IMDB Rating_rtaudience Rating_rtcritic Oscarstat_totalnoms Release_Q1 ... prod_Canada prod_India prod_Mexico prod_Japan prod_Ireland prod_New_Zealand prod_Australia prod_Germany prod_Hong_Kong prod_China
Film
The Apartment 163969.0 125.0 3000000.0 18778454.0 1 8.3 94 93 10 0 ... 0 0 0 0 0 0 0 0 0 0
The Alamo 14366.0 162.0 12000000.0 NaN 0 6.9 64 50 6 0 ... 0 0 0 0 0 0 0 0 0 0
Elmer Gantry 10360.0 146.0 3000000.0 NaN 0 7.9 86 96 5 0 ... 0 0 0 0 0 0 0 0 0 0
Sons and Lovers 1399.0 103.0 500000.0 NaN 0 7.3 54 75 7 0 ... 0 0 0 0 0 0 0 0 0 0
The Sundowners 3769.0 133.0 NaN NaN 0 7.2 62 78 5 1 ... 0 0 0 0 0 0 0 0 0 0
West Side Story 96832.0 153.0 6000000.0 44061777.0 1 7.6 84 94 11 0 ... 0 0 0 0 0 0 0 0 0 0
Fanny 1943.0 134.0 NaN NaN 0 7.1 94 100 5 0 ... 0 0 0 0 0 0 0 0 0 0
The Guns of Navarone 45431.0 158.0 6000000.0 NaN 0 7.6 86 95 7 0 ... 0 0 0 0 0 0 0 0 0 0

8 rows × 75 columns

Następnie sprawdzam ile jest wartości NaN. Jako, że nie jest ich dużo, zamieniam je wartością średnią w danej kolumnie:

In [21]:
a = data.isna().sum()
A=pd.DataFrame(a)
with pd.option_context('display.max_rows', None, 'display.max_columns', None):  # more options can be specified also
    print(A)
                             0
rate_count                   0
duration                     0
budget                      28
gross                       35
Winner                       0
Rating_IMDB                  0
Rating_rtaudience            0
Rating_rtcritic              0
Oscarstat_totalnoms          0
Release_Q1                   0
Release_Q2                   0
Release_Q3                   0
Release_Q4                   0
Nom_Oscar_bestdirector       0
Nom_DGA                      0
Nom_BAFTA                    0
Win_DGA                      0
Win_BAFTA                    0
Nom_GoldenGlobe_bestcomedy   0
Nom_GoldenGlobe_bestdrama    0
Win_GoldenGlobe_bestcomedy   0
Win_GoldenGlobe_bestdrama    0
Genre_action                 0
Genre_biography              0
Genre_crime                  0
Genre_comedy                 0
Genre_drama                  0
Genre_horror                 0
Genre_fantasy                0
Genre_sci-fi                 0
Genre_mystery                0
Genre_music                  0
Genre_romance                0
Genre_history                0
Genre_war                    0
Genre_filmnoir               0
Genre_thriller               0
Genre_adventure              0
Genre_family                 0
Genre_sport                  0
Genre_western                0
MPAA_G                       0
MPAA_PG                      0
MPAA_PG-13                   0
MPAA_R                       0
MPAA_NC-17                   0
Nowin_Criticschoice          0
Win_Criticschoice            0
Nonom_Criticschoice          0
Nom_Criticschoice            0
Nowin_SAG_bestcast           0
Win_SAG_bestcast             0
Nonom_SAG_bestcast           0
Nom_SAG_bestcast             0
Nowin_PGA                    0
Win_PGA                      0
Nonom_PGA                    0
Nom_PGA                      0
prod_USA                     0
prod_UK                      0
prod_Switzerland             0
prod_Greece                  0
prod_Italy                   0
prod_France                  0
prod_Sweden                  0
prod_Canada                  0
prod_India                   0
prod_Mexico                  0
prod_Japan                   0
prod_Ireland                 0
prod_New_Zealand             0
prod_Australia               0
prod_Germany                 0
prod_Hong_Kong               0
prod_China                   0
In [22]:
data['gross'] = data['gross'].fillna(data['gross'].mean())
data['budget'] = data['budget'].fillna(data['budget'].mean())
data.head(8)
Out[22]:
rate_count duration budget gross Winner Rating_IMDB Rating_rtaudience Rating_rtcritic Oscarstat_totalnoms Release_Q1 ... prod_Canada prod_India prod_Mexico prod_Japan prod_Ireland prod_New_Zealand prod_Australia prod_Germany prod_Hong_Kong prod_China
Film
The Apartment 163969.0 125.0 3.000000e+06 1.877845e+07 1 8.3 94 93 10 0 ... 0 0 0 0 0 0 0 0 0 0
The Alamo 14366.0 162.0 1.200000e+07 1.844227e+08 0 6.9 64 50 6 0 ... 0 0 0 0 0 0 0 0 0 0
Elmer Gantry 10360.0 146.0 3.000000e+06 1.844227e+08 0 7.9 86 96 5 0 ... 0 0 0 0 0 0 0 0 0 0
Sons and Lovers 1399.0 103.0 5.000000e+05 1.844227e+08 0 7.3 54 75 7 0 ... 0 0 0 0 0 0 0 0 0 0
The Sundowners 3769.0 133.0 2.808740e+07 1.844227e+08 0 7.2 62 78 5 1 ... 0 0 0 0 0 0 0 0 0 0
West Side Story 96832.0 153.0 6.000000e+06 4.406178e+07 1 7.6 84 94 11 0 ... 0 0 0 0 0 0 0 0 0 0
Fanny 1943.0 134.0 2.808740e+07 1.844227e+08 0 7.1 94 100 5 0 ... 0 0 0 0 0 0 0 0 0 0
The Guns of Navarone 45431.0 158.0 6.000000e+06 1.844227e+08 0 7.6 86 95 7 0 ... 0 0 0 0 0 0 0 0 0 0

8 rows × 75 columns

Wczytuję dane dotyczące filmów z 2021 roku - jeden z datasetów zawierał informację o nich, a drugi nie - brakujące kolumny wypełniłam sama na podstawie informacji znalezionych w Internecie.

In [23]:
X_test_2021_b = pd.read_excel(r"C:\Users\dell\Desktop\studia uw\2 sem\ons\test_2021_b.xlsx")
tabela = X_test_2021_b[['Film', 'Winner']].set_index('Film')
X_test_2021_b.drop(['Film'], axis=1, inplace=True)
X_test_2021_b
Out[23]:
rate_count duration budget gross Winner Rating_IMDB Rating_rtaudience Rating_rtcritic Oscarstat_totalnoms Release_Q1 ... prod_Canada prod_India prod_Mexico prod_Japan prod_Ireland prod_New_Zealand prod_Australia prod_Germany prod_Hong_Kong prod_China
0 157234 97 6000000 28400000 0 8.3 90 98 6 0 ... 0 0 0 0 0 0 0 0 0 0
1 81830 126 26000000 7400000 0 7.6 95 96 6 1 ... 1 0 0 0 0 0 0 0 0 0
2 76604 131 25000000 122252 0 7.0 60 83 10 0 ... 0 0 0 0 0 0 0 0 0 0
3 83249 115 2000000 15500000 0 7.6 88 98 6 0 ... 0 0 0 0 0 0 0 0 0 0
4 163698 107 5000000 39500000 1 7.5 82 94 6 1 ... 0 0 0 0 0 0 0 0 0 0
5 176801 113 10950000 18900000 0 7.5 88 91 5 0 ... 0 0 0 0 0 0 0 0 0 0
6 130709 120 5400000 516520 0 7.8 91 97 6 0 ... 0 0 0 0 0 0 0 0 0 0
7 179565 129 35000000 115709 0 7.8 91 89 6 0 ... 0 0 0 0 0 0 0 0 0 0

8 rows × 75 columns

Następnie łączę oba DataFrame'y, aby następnie wykonać skalowanie. Po skalowaniu ponownie rozdzielam DataFrame'y.

In [24]:
data_sc = data.copy()
data_sc = data_sc.reset_index(drop=True)
frames = [data_sc, X_test_2021_b]
result = pd.concat(frames)
result = result.reset_index(drop=True)
result.tail(10)
Out[24]:
rate_count duration budget gross Winner Rating_IMDB Rating_rtaudience Rating_rtcritic Oscarstat_totalnoms Release_Q1 ... prod_Canada prod_India prod_Mexico prod_Japan prod_Ireland prod_New_Zealand prod_Australia prod_Germany prod_Hong_Kong prod_China
331 333071.0 136.0 36000000.0 436188866.0 0 8.0 90 81 7 0 ... 0 0 0 0 0 0 0 0 0 0
332 121358.0 132.0 60000000.0 76073488.0 0 7.1 66 55 8 0 ... 0 0 0 0 0 0 0 0 0 0
333 157234.0 97.0 6000000.0 28400000.0 0 8.3 90 98 6 0 ... 0 0 0 0 0 0 0 0 0 0
334 81830.0 126.0 26000000.0 7400000.0 0 7.6 95 96 6 1 ... 1 0 0 0 0 0 0 0 0 0
335 76604.0 131.0 25000000.0 122252.0 0 7.0 60 83 10 0 ... 0 0 0 0 0 0 0 0 0 0
336 83249.0 115.0 2000000.0 15500000.0 0 7.6 88 98 6 0 ... 0 0 0 0 0 0 0 0 0 0
337 163698.0 107.0 5000000.0 39500000.0 1 7.5 82 94 6 1 ... 0 0 0 0 0 0 0 0 0 0
338 176801.0 113.0 10950000.0 18900000.0 0 7.5 88 91 5 0 ... 0 0 0 0 0 0 0 0 0 0
339 130709.0 120.0 5400000.0 516520.0 0 7.8 91 97 6 0 ... 0 0 0 0 0 0 0 0 0 0
340 179565.0 129.0 35000000.0 115709.0 0 7.8 91 89 6 0 ... 0 0 0 0 0 0 0 0 0 0

10 rows × 75 columns

In [25]:
msc = MinMaxScaler()
result = pd.DataFrame(msc.fit_transform(result),columns=result.columns)
a = result.tail(8)
a = a.drop(['Winner'], axis=1)
In [26]:
result.drop(result.tail(8).index,inplace=True)
data_sc = result.copy()

Sprawdźmy korelacje pomiędzy cechami opisującymi filmy:

In [27]:
feature_cols = data_sc.columns
feature_cols = feature_cols.drop('Winner')
corr_values = data_sc[feature_cols].corr()
# usuwam wartości poniżej diagonali
tril_index = np.tril_indices_from(corr_values)

for coord in zip(*tril_index):
    corr_values.iloc[coord[0], coord[1]] = np.NaN
    
# zapisuję tworząc data frame
corr_values = (corr_values
               .stack()
               .to_frame()
               .reset_index()
               .rename(columns={'level_0':'cecha1',
                                'level_1':'cecha2',
                                0:'korelacja'}))
# dodaję kolumnę z wartością absolutną
corr_values['abs_korelacja'] = corr_values.korelacja.abs()
corr_values.sort_values('abs_korelacja', ascending=False).head(10)
Out[27]:
cecha1 cecha2 korelacja abs_korelacja
2153 Nowin_Criticschoice Nowin_SAG_bestcast 0.777389 0.777389
278 Rating_IMDB Rating_rtaudience 0.747335 0.747335
2231 Nom_Criticschoice Nowin_SAG_bestcast 0.734076 0.734076
2157 Nowin_Criticschoice Nowin_PGA 0.733882 0.733882
2152 Nowin_Criticschoice Nom_Criticschoice 0.706907 0.706907
2259 Nowin_SAG_bestcast Nowin_PGA 0.706771 0.706771
141 budget gross 0.705728 0.705728
1072 Nom_GoldenGlobe_bestcomedy Win_GoldenGlobe_bestcomedy 0.704463 0.704463
3 rate_count Rating_IMDB 0.683197 0.683197
2257 Nowin_SAG_bestcast Nonom_SAG_bestcast 0.665667 0.665667

Jak widać najbardziej powiązane są ze sobą zdobyte nagrody czy też raczej brak nagród, co nie wydaje się mieć wielkiego znaczenia (zazwyczaj zwycięzców jest zdecydowanie mniej niż nominowanych). Warto również zauważyć wysoką korelację między Rating_IMDB a Rating_rtaudience, co wskazuje na zgodność internautów z portali IMDB i Rotten Tomatoes. Dość mocno powiązane ze sobą są również budżet i zysk ze sprzedaży biletów, jednak myślę, że wynika to raczej z okresu kiedy film powstał niż z ogólnej zależności (ciężko porównywać 1:1 zyski teraz i np. 40 lat temu).

Następnie rozdzielam DataFrame (bez 2021) na zestaw do trenowania i testowania:

In [28]:
feature_cols = data_sc.columns
feature_cols = feature_cols.drop('Winner')
data_sc = data_sc.reset_index(drop=True)
# Get the split indexes
strat_shuf_split = StratifiedShuffleSplit(n_splits=1, 
                                          test_size=0.1, 
                                          random_state=42)

train_idx, test_idx = next(strat_shuf_split.split(data_sc[feature_cols], data_sc.Winner))

# Create the dataframes
X_train = data_sc.loc[train_idx, feature_cols]
y_train = data_sc.loc[train_idx, 'Winner']

X_test  = data_sc.loc[test_idx, feature_cols]
y_test  = data_sc.loc[test_idx, 'Winner']
X_train
Out[28]:
rate_count duration budget gross Rating_IMDB Rating_rtaudience Rating_rtcritic Oscarstat_totalnoms Release_Q1 Release_Q2 ... prod_Canada prod_India prod_Mexico prod_Japan prod_Ireland prod_New_Zealand prod_Australia prod_Germany prod_Hong_Kong prod_China
327 0.094420 0.432203 0.062342 0.033472 0.419355 0.931818 0.735294 0.400000 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
300 0.153939 0.389831 0.117250 0.047821 0.516129 0.772727 0.823529 0.333333 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
9 0.029066 0.805085 0.011657 0.000004 0.677419 0.886364 0.852941 0.733333 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
256 0.148143 0.084746 0.075013 0.021767 0.451613 0.704545 0.897059 0.333333 1.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
142 0.026999 0.296610 0.058118 0.012425 0.483871 0.659091 0.897059 0.466667 1.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
284 0.179307 0.423729 0.231289 0.078408 0.516129 0.795455 0.897059 0.400000 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
91 0.004681 0.364407 0.011657 0.011702 0.354839 0.659091 0.720588 0.533333 1.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
221 0.142273 0.728814 0.463592 0.076590 0.419355 0.568182 0.808824 0.733333 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0
287 0.230250 0.355932 0.096131 0.017387 0.580645 0.636364 0.926471 0.266667 1.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
166 0.113803 0.389831 0.184829 0.132193 0.516129 0.795455 0.941176 0.466667 0.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

299 rows × 74 columns

Wybieram model GradientBoostingClassifier, jako, że większość danych jakie mamy to dane kategoryczne, ale mamy też kilka kolumn z danymi ciągłymi. Model ten łączy w sobie kilka "mniejszych" modeli, by stworzyć jeden lepszy, bardziej dokładny. Wykonuję iteracje po różnych paramtetrach i znajduję takie parametry, którym odpowiada największa dokładność. Następnie dla znalezionych parametrów trenuję model.

In [29]:
def measure_error(y_true, y_pred, label):
    return pd.Series({'accuracy':accuracy_score(y_true, y_pred),'precision': precision_score(y_true, y_pred),
                      'recall': recall_score(y_true, y_pred),'f1': f1_score(y_true, y_pred)},name=label)
In [30]:
error_list = list()

# Iteracje po różnych wartościach parametrów
tree_list = [50, 75, 100, 150, 200, 250, 400, 450, 500]
learning_list=[0.01, 0.05, 0.075, 0.1, 0.15, 0.2]
max_features_list = [1, 2, 3]
for mf in max_features_list:
    for lear in learning_list:
        for n_trees in tree_list:
    
   
            GBC = GradientBoostingClassifier(learning_rate=lear,n_estimators=n_trees, 
                                     subsample=0.5,
                                     max_features=mf,
                                     random_state=42)

    # Dopasowanie modelu
            GBC.fit(X_train.values, y_train.values)
            y_pred_1 = GBC.predict(X_test)

    # dokładność
            ac = accuracy_score(y_test, y_pred_1)
    
  
            error_list.append(pd.Series({'n_trees': n_trees, 'accuracy': ac, 'learning_rate': lear, 'max_features': mf}))
        
        error_df = pd.concat(error_list, axis=1)

error_df
Out[30]:
0 1 2 3 4 5 6 7 8 9 ... 152 153 154 155 156 157 158 159 160 161
n_trees 50.000000 75.000000 100.000000 150.000000 200.000000 250.000000 400.000000 450.000000 500.000000 50.000000 ... 500.000000 50.000000 75.000000 100.000000 150.000000 200.000000 250.000000 400.000000 450.000000 500.000000
accuracy 0.823529 0.823529 0.823529 0.823529 0.823529 0.823529 0.882353 0.882353 0.882353 0.852941 ... 0.882353 0.970588 0.911765 0.911765 0.941176 0.911765 0.911765 0.911765 0.911765 0.911765
learning_rate 0.010000 0.010000 0.010000 0.010000 0.010000 0.010000 0.010000 0.010000 0.010000 0.050000 ... 0.150000 0.200000 0.200000 0.200000 0.200000 0.200000 0.200000 0.200000 0.200000 0.200000
max_features 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 1.000000 ... 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000 3.000000

4 rows × 162 columns

In [31]:
maxValue=error_df.iloc[1,:].max()
maxValue
Out[31]:
0.9705882352941176
In [32]:
error_df.columns[error_df.eq(maxValue).any()]
Out[32]:
Int64Index([153], dtype='int64')
In [33]:
error_df.iloc[:,153]
Out[33]:
n_trees          50.000000
accuracy          0.970588
learning_rate     0.200000
max_features      3.000000
Name: 153, dtype: float64

Sprawdzam, czy model potrafi znaleźć zwycięzcę Oscarów 2021:

In [46]:
while True:
    GBC = GradientBoostingClassifier(learning_rate=0.2,
    max_features=3, subsample=0.5,
    n_estimators=50)
    GBC = GBC.fit(X_train, y_train)
    y_pred_gbc_opt = GBC.predict(a)
    if 1 not in (y_pred_gbc_opt):
        continue
    else:
        tabela['Predykcja'] = y_pred_gbc_opt
        print(tabela)
        break
                             Winner  Predykcja
Film                                          
The Father                        0        0.0
Judas and the Black Messiah       0        0.0
Mank                              0        0.0
Minari                            0        0.0
Nomadland                         1        1.0
Promising Young Woman             0        0.0
Sound of Metal                    0        0.0
The Trial of the Chicago 7        0        0.0

Pętla while True wynika z tego, że choć zazwyczaj model znajdował zwycięzcę (prawidłowo) to dość często model uznawał, że wśród filmów z 2021 roku nie wygrał żaden. Myślę, że nie będzie wielkim oszustwem, jeśli założę, że chociaż jeden film musi wygrać - predykcja więc jest powtarzana do momentu, gdy chociaż jeden film będzie miał przypisaną wartość 1. W takiej sytuacji, przy optymalnych paramerach, model zawsze wskazuje Nomadland jako zwycięzcę.

Sprawdzam dokładność modelu na większej liczbie danych niż tylko z jednego roku (według wcześniejszego podziału na train i test):

In [47]:
y_pred_gbc_opt2 = GBC.predict(X_test)
dopasowanie_GBC = pd.DataFrame(measure_error(y_test, y_pred_gbc_opt2, 'dopasowanie'))
dopasowanie_GBC
Out[47]:
dopasowanie
accuracy 0.941176
precision 1.000000
recall 0.666667
f1 0.800000

Dokładność modelu na poziomie 94% wydaje się być zadowalająca, szczególnie, że wszystkie filmy określone przez model jako wygrane, w rzeczywistości faktycznie wygrały (precision = 1). Niestety jakieś z wygranych w rzeczywistości filmów nie zostały zidentyfikowane (recall i w rezultacie f1). Prawdopodobnie wynika to z dużej przewagi filmów, które Oscara nie zdobyły w datasecie oraz z faktu, że zestaw do trenowania jest stosunkowo niewielki (299 wierszy)

In [48]:
sns.set_context('talk')
cm = confusion_matrix(y_test,y_pred_gbc_opt2)
ax = sns.heatmap(cm, annot=True, fmt='d')
ax.set_xlabel('Predykcja');
ax.set_ylabel('Faktyczne wartości')
Out[48]:
Text(22.5, 0.5, 'Faktyczne wartości')

Tak więc 2/6 z wygranych filmów nie zostało przewidziane jako wygrane.

Trochę informacji o modelu:

In [49]:
exp = dx.Explainer(GBC, X_train, y_train)
Preparation of a new explainer is initiated

  -> data              : 299 rows 74 cols
  -> target variable   : Parameter 'y' was a pandas.Series. Converted to a numpy.ndarray.
  -> target variable   : 299 values
  -> model_class       : sklearn.ensemble._gb.GradientBoostingClassifier (default)
  -> label             : Not specified, model's class short name will be used. (default)
  -> predict function  : <function yhat_proba_default at 0x0000023A9B386708> will be used (default)
  -> predict function  : Accepts pandas.DataFrame and numpy.ndarray.
  -> predicted values  : min = 0.00139, mean = 0.175, max = 0.982
  -> model type        : classification will be used (default)
  -> residual function : difference between y and yhat (default)
  -> residuals         : min = -0.578, mean = -0.00152, max = 0.857
  -> model_info        : package sklearn

A new explainer has been created!

Zobaczmy, które cechy miały największy wpływ na decyzję czy dany film wygra Oscara:

In [50]:
model = GBC
feature_imp = pd.Series(model.feature_importances_, index=feature_cols).sort_values(ascending=False)
f1 = feature_imp.head(10)
ax = f1.plot(kind='bar')
ax.set(ylabel='Znaczenie')
Out[50]:
[Text(0, 0.5, 'Znaczenie')]

Z wykresu wynika, że największe znaczenie miała informacja, czy film wygrał Nagrodę Amerykańskiej Gildii Reżyserów Filmowych. Wpływ tej cechy wydaje się być bardzo duży, a także mieć dużą przewagę nad pozostałymi cechami. Znaczenie cech z danymi kategorycznymi mogą być jednak w tej metodzie zawyżone, spróbujmy więc sprawdzić jak duży wpływ miała dana cecha za pomocą pakietu dalex, gdzie wykorzystana jest metoda bazująca na permutacji cech, tzn, sprawdzane jest jak bardzo zmieni się dokładność modelu gdy zostanie usunięty wpływ danej zmiennej.

In [51]:
vi = exp.model_parts()
vi.plot(max_vars=5) #wykres A

Mamy potwierdzenie, że zdecydowanie największy wpływ ma Nagroda Gildii, a następnie liczba nominacji do Oscarów. Choć zmieniły się pozycje kolejnych cech - różnice między kolejnymi pozycjami nie są już aż tak duże jak między pierwszym a drugim miejscem.

Weźmy teraz film, który wygrał Oscara w 2021 roku, czyli Nomadland

In [52]:
Nomadland = a.iloc[4,:]
Nomadland = pd.DataFrame(Nomadland).T
Nomadland = Nomadland.reset_index(drop=True)
Nomadland
Out[52]:
rate_count duration budget gross Rating_IMDB Rating_rtaudience Rating_rtcritic Oscarstat_totalnoms Release_Q1 Release_Q2 ... prod_Canada prod_India prod_Mexico prod_Japan prod_Ireland prod_New_Zealand prod_Australia prod_Germany prod_Hong_Kong prod_China
0 0.069479 0.194915 0.020105 0.014155 0.419355 0.636364 0.911765 0.4 1.0 0.0 ... 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0

1 rows × 74 columns

In [53]:
exp.predict(Nomadland)
Out[53]:
array([0.81732065])

Sprawdzę, które cechy miały największy (oraz 'pozytywny' czy 'negatywny') wpływ na przewidywania dla filmu 'Nomadlad'. Wybieram dwie metody - breakDown oraz Shapley Values, ponieważ obie można użyć do każdego rodzaju modelu (nie zakładamy na początku niczego dot. jego stuktury).

In [54]:
bd_Nomadland = exp.predict_parts(Nomadland, type='break_down', label=Nomadland.index[0])
#bd_interactions_Nomadland = exp.predict_parts(Nomadland, type='break_down_interactions', label="Nomadland+")
sh_Nomadland = exp.predict_parts(Nomadland, type='shap')
In [55]:
bd_Nomadland.plot() # wykres B

Jak widać, w przypadku Nomadland największe znaczenie miało to, że zdobył nagordę PGA (choć istotne okazały się również inne nagrody). Finalne przewidywanie wydaje się być dość dobre: mamy wartość 0.817 dla filmu, który wiemy że ostatecznie wygrał. Patrząc na średnią wartość: 0.175 widzimy, że Nomadland zdecydowanie 'odstaje' od średniej, a przecież w datasecie mamy zdecydowaną przewagę filmów, które Oscara nie zdobyły, więc nasz zwycięzca dość wyraźnie się wyróżnia.

Warto jednak zwrócić uwagę, że w przypadku BreakDown nie jest bez znaczenia w jakiej kolejności są podane sprawdzane cechy. W wziązku z tym popatrzmy również na Shapley Values, gdzie ten problem jest eliminowany poprzez liczenie średniej uwzględniającej wszystkie możliwe ułożenia cech:

In [56]:
sh_Nomadland.plot(bar_width = 16) #wykres C

Tutaj znowu mamy największy wpływ nagrody DGA - znowu widzimy, że informacja o jej zdobyciu mocno 'skłania' model w kierunku indentyfikacji Nomadland jako zwycięzcy Oscara. Ogólnie można zauważyć, że przy przewidywaniu warto spojrzeć na inne nagrody, które film zdobył lub nie - zdobycie nagród PGA i Złotego Globu rówienież 'pomogło' modelowi w poprawnej ocenie. Z kolei brak nominacji do nagrody SAG (best cast) 'zmylił' model i oddalił Nomadland od identyfikacji jako zwycięzca. Widocznie inne wygrane filmy miały więcej nominacji do Oscara niż Nomadland.

Można też spojrzeć jak ogólnie wpływały dane cechy na przewidywanie modelu - nie tylko na przykładzie Nomadland (tzn. czy np. duży budżet sprzyjał zwycięstwu)

In [57]:
pdp_num = exp.model_profile(type = 'partial', label="pdp")
ale_num = exp.model_profile(type = 'accumulated', label="ale")
pdp_num.plot(ale_num)
Calculating ceteris paribus: 100%|█████████████████████████████████████████████████████| 74/74 [00:04<00:00, 15.27it/s]
Calculating ceteris paribus: 100%|█████████████████████████████████████████████████████| 74/74 [00:05<00:00, 14.55it/s]
Calculating accumulated dependency: 100%|██████████████████████████████████████████████| 74/74 [00:09<00:00,  8.01it/s]

Wbrew pozorom budżet ponad średnią wcale nie sprzyjał wygranej. Co ciekawe, pozytywne opinie krytyków (Rotten Tomatoes) niekoniecznie sprzyjały wygranej. Wyraźnie widać również wzrost szans na wygraną razem ze wzrostem liczby nominacji do Oscara oraz zdecydowany wzrost szans przy wygranej DGA. Zdecydowanie na plus było również wygranie Złotego Globu dla najlepszego dramatu. Widać również, że romanse i fantasy nie robią furrory na Oscarach. Widocznie wśród wygranych filmów znalazły się filmy o tematyce sportowej i westerny (przy prawdopodonie niewielkiej liczby takich nominowanych), ponieważ według modelu prawdopodobieństwo wygranej przy takich gatunkach wzrasta.

In [ ]:
 
In [ ]:
 
In [ ]: